﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;

namespace Notoric.Model.Collections
{
    /// <summary>
    /// Represents a dynamic data collection that provides notifications when items get added, removed, or when the whole list is refreshed.
    /// </summary>
    /// <typeparam name="T">Type of the collection items</typeparam>
    public class ObservableCollection<T> : Collection<T>, INotifyCollectionChanged, INotifyPropertyChanged
    {
        #region Constructors

        /// <summary>
        /// Initializes a new <see cref="ObservableCollection{T}"/> instance.
        /// </summary>
        public ObservableCollection()
        {
        }

        /// <summary>
        /// Initializes a new <see cref="ObservableCollection{T}"/> instance that contains
        /// elements copied from the specified collection.
        /// </summary>
        /// <param name="collection">
        /// <para>
        /// Type: <see cref="IEnumerable{T}"/>
        /// </para>
        /// <para>
        /// Source collection.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="collection"/> is <see langword="null"/>.
        /// </exception>
        public ObservableCollection(IEnumerable<T> collection)
        {
            if (collection == null)
                throw new ArgumentNullException("collection");

            CopyFrom(collection);
        }

        /// <summary>
        /// Initializes a new <see cref="ObservableCollection{T}"/> instance that contains
        /// elements copied from the specified list.
        /// </summary>
        /// <param name="collection">
        /// <para>
        /// Type: <see cref="List{T}"/>
        /// </para>
        /// <para>
        /// Source list.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="collection"/> is <see langword="null"/>.
        /// </exception>
        public ObservableCollection(List<T> collection)
        {
            if (collection == null)
                throw new ArgumentNullException("collection");

            CopyFrom(collection);
        }

        #endregion

        #region INotifyCollectionChanged Members

        /// <inheritdoc/>
        public event EventHandler<NotifyCollectionChangedEventArgs> CollectionChanged;

        #endregion

        #region INotifyPropertyChanged Members

        /// <summary>
        /// Occurs when the value of some property has changed.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

        #region Collection<T> Members

        /// <inheritdoc/>
        protected override void ClearItems()
        {
            base.ClearItems();

            RaisePropertyChanged("Count");
            RaisePropertyChanged("Item[]");
            RaiseCollectionChanged(new NotifyCollectionChangedEventArgs());
        }

        /// <inheritdoc/>
        protected override void InsertItem(int index, T item)
        {
            base.InsertItem(index, item);

            RaisePropertyChanged("Count");
            RaisePropertyChanged("Item[]");
            RaiseCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index));
        }

        /// <inheritdoc/>
        protected override void RemoveItem(int index)
        {
            T oldItem = base[index];
            base.RemoveItem(index);

            RaisePropertyChanged("Count");
            RaisePropertyChanged("Item[]");
            RaiseCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, oldItem, index));
        }

        /// <inheritdoc/>
        protected override void SetItem(int index, T item)
        {
            T oldItem = base[index];
            base.SetItem(index, item);

            RaisePropertyChanged("Item");
            RaiseCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, oldItem, item, index));
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Copies the content of a given collection to this collection.
        /// </summary>
        /// <param name="collection">
        /// <para>
        /// Type: <see cref="IEnumerable{T}"/>
        /// </para>
        /// <para>
        /// Source collection.
        /// </para>
        /// </param>
        private void CopyFrom(IEnumerable<T> collection)
        {
            IList<T> items = base.Items;
            if ((collection != null) && (items != null))
            {
                using (IEnumerator<T> enumerator = collection.GetEnumerator())
                {
                    while (enumerator.MoveNext())
                    {
                        items.Add(enumerator.Current);
                    }
                }
            }
        }

        #endregion

        #region Protected Methods

        /// <summary>
        /// Moves the item at the specified index to a new location in the collection.
        /// </summary>
        /// <remarks>
        /// <para>
        /// This implementation raises the <see cref="CollectionChanged"/> event.
        /// </para>
        /// <para>
        /// Subclasses can override this protected method to provide custom behaviour to the <see cref="Move"/> method.
        /// </para>
        /// </remarks>
        /// <param name="oldIndex">
        /// <para>
        /// Type: <see cref="System.Int32"/>
        /// </para>
        /// <para>
        /// The zero-based index specifying the location of the item to be moved.
        /// </para>
        /// </param>
        /// <param name="newIndex">
        /// <para>
        /// Type: <see cref="System.Int32"/>
        /// </para>
        /// <para>
        /// The zero-based index specifying the new location of the item.
        /// </para>
        /// </param>
        protected virtual void MoveItem(int oldIndex, int newIndex)
        {
            T item = base[oldIndex];
            base.RemoveItem(oldIndex);
            base.InsertItem(newIndex, item);

            RaisePropertyChanged("Item[]");
            RaiseCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, item, newIndex, oldIndex));
        }

        /// <summary>
        /// Raises the <see cref="CollectionChanged"/> event on this <see cref="ElementCollection{T}"/> instance.
        /// </summary>
        /// <param name="e"></param>
        protected void RaiseCollectionChanged(NotifyCollectionChangedEventArgs e)
        {
            if (CollectionChanged != null)
                CollectionChanged(this, e);
        }

        /// <summary>
        /// Raises the <see cref="PropertyChanged"/> event on this <see cref="ElementCollection{T}"/> instance.
        /// </summary>
        /// <param name="propertyName">
        /// <para>
        /// Type: <see cref="System.String"/>
        /// </para>
        /// <para>
        /// Name of the property whose value has changed.
        /// </para>
        /// </param>
        protected void RaisePropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Searches a range of elements in the sorted collection for an element using the specified comparer and returns the zero-based index of the element.
        /// </summary>
        /// <param name="index">
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The zero-based starting index of the range to search.
        /// </para>
        /// </param>
        /// <param name="count">
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The length of the range to search.
        /// </para>
        /// </param>
        /// <param name="item">
        /// <para>
        /// Type: <typeparamref name="T"/>
        /// </para>
        /// <para>
        /// The object to locate. The value can be <see langword="null"/> for reference types.
        /// </para>
        /// </param>
        /// <param name="comparer">
        /// <para>
        /// Type: <see cref="IComparer{T}"/> of <typeparamref name="T"/>
        /// </para>
        /// <para>
        /// The <see cref="IComparer{T}"/> implementation to use when comparing elements, or <see langword="null"/> to use the default comparer <see cref="Comparer{T}.Default"/>.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The zero-based index of item in the sorted collection, if item is found; otherwise -1.
        /// </para>
        /// </returns>
        /// <exception cref="ArgumentOutOfRangeException">
        /// <paramref name="index"/> is less than 0.<br/>
        /// -or-<br/>
        /// <paramref name="count"/> is less than 0.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// <paramref name="index"/> and <paramref name="count"/> do not denote a valid range in the collection.
        /// </exception>
        /// <exception cref="InvalidOperationException">
        /// <paramref name="comparer"/> is <see langword="null"/> and the default comparer <see cref="Comparer{T}.Default"/> cannot
        /// find an implementation of the <see cref="IComparable{T}"/> generic interface or the <see cref="IComparable"/> interface for type <typeparamref name="T"/>.
        /// </exception>
        public int BinarySearch(int index, int count, T item, IComparer<T> comparer)
        {
            if (index < 0)
                throw new ArgumentOutOfRangeException("index", "Index is less than 0!");
            if (count < 0)
                throw new ArgumentOutOfRangeException("count", "Count is less than 0!");
            if (index + count > base.Count)
                throw new ArgumentException("Index and count do not denote a valid range!");
            if (comparer == null)
            {
                try
                {
                    comparer = Comparer<T>.Default;
                }
                catch (Exception)
                {
                    throw new InvalidOperationException("Default comparer cannot find an implementation of IComparable<T> interface for the type " + typeof(T).FullName + "!");
                }
            }
            
            int l = index;
            int u = index + count - 1;

            while (l <= u)
            {
                int i = l + ((u - l) >> 1);
                int r = comparer.Compare(base.Items[i], item);

                if (r == 0)
                    return i;
                else if (r < 0)
                    l = i + 1;
                else
                    u = i - 1;
            }

            return -1;
        }

        /// <summary>
        /// Searches the entire sorted collection for an element using the specified comparer and returns the zero-based index of the element.
        /// </summary>
        /// <param name="item">
        /// <para>
        /// Type: <typeparamref name="T"/>
        /// </para>
        /// <para>
        /// The object to locate. The value can be <see langword="null"/> for reference types.
        /// </para>
        /// </param>
        /// <param name="comparer">
        /// <para>
        /// Type: <see cref="IComparer{T}"/> of <typeparamref name="T"/>
        /// </para>
        /// <para>
        /// The <see cref="IComparer{T}"/> implementation to use when comparing elements, or <see langword="null"/> to use the default comparer <see cref="Comparer{T}.Default"/>.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The zero-based index of item in the sorted collection, if item is found; otherwise -1.
        /// </para>
        /// </returns>
        /// <exception cref="InvalidOperationException">
        /// <paramref name="comparer"/> is <see langword="null"/> and the default comparer <see cref="Comparer{T}.Default"/> cannot
        /// find an implementation of the <see cref="IComparable{T}"/> generic interface or the <see cref="IComparable"/> interface for type <typeparamref name="T"/>.
        /// </exception>
        public int BinarySearch(T item, IComparer<T> comparer)
        {
            return BinarySearch(0, base.Count, item, comparer);
        }

        /// <summary>
        /// Searches the entire sorted collection for an element using the default comparer and returns the zero-based index of the element.
        /// </summary>
        /// <param name="item">
        /// <para>
        /// Type: <typeparamref name="T"/>
        /// </para>
        /// <para>
        /// The object to locate. The value can be <see langword="null"/> for reference types.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The zero-based index of item in the sorted collection, if item is found; otherwise -1.
        /// </para>
        /// </returns>
        /// <exception cref="InvalidOperationException">
        /// The default comparer <see cref="Comparer{T}.Default"/> cannot find an implementation of the 
        /// <see cref="IComparable{T}"/> generic interface or the <see cref="IComparable"/> interface for type <typeparamref name="T"/>.
        /// </exception>
        public int BinarySearch(T item)
        {
            return BinarySearch(0, base.Count, item, null);
        }

        /// <summary>
        /// Searches a range of elements in the sorted collection for an element using the specified compare method and returns the zero-based index of the element.
        /// </summary>
        /// <param name="index">
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The zero-based starting index of the range to search.
        /// </para>
        /// </param>
        /// <param name="count">
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The length of the range to search.
        /// </para>
        /// </param>
        /// <param name="predicate">
        /// <para>
        /// Type: <see cref="SortedSearchPredicate{T}"/> of <typeparamref name="T"/>
        /// </para>
        /// <para>
        /// The method that compares the items in the collection to the searched element.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The zero-based index of item in the sorted collection, if item is found; otherwise -1.
        /// </para>
        /// </returns>
        /// <exception cref="ArgumentOutOfRangeException">
        /// <paramref name="index"/> is less than 0.<br/>
        /// -or-<br/>
        /// <paramref name="count"/> is less than 0.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// <paramref name="index"/> and <paramref name="count"/> do not denote a valid range in the collection.
        /// </exception>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="predicate"/> is <see langword="null"/>.
        /// </exception>
        public int BinarySearch(int index, int count, SortedSearchPredicate<T> predicate)
        {
            if (index < 0)
                throw new ArgumentOutOfRangeException("index", "Index is less than 0!");
            if (count < 0)
                throw new ArgumentOutOfRangeException("count", "Count is less than 0!");
            if (index + count > base.Count)
                throw new ArgumentException("Index and count do not denote a valid range!");
            if (predicate == null)
                throw new ArgumentNullException("predicate");

            int l = index;
            int u = index + count - 1;

            while (l <= u)
            {
                int i = l + ((u - l) >> 1);
                int r = predicate(base.Items[i]);

                if (r == 0)
                    return i;
                else if (r < 0)
                    l = i + 1;
                else
                    u = i - 1;
            }

            return -1;
        }

        /// <summary>
        /// Searches the entire sorted collection for an element using the specified compare method and returns the zero-based index of the element.
        /// </summary>
        /// <param name="predicate">
        /// <para>
        /// Type: <see cref="SortedSearchPredicate{T}"/> of <typeparamref name="T"/>
        /// </para>
        /// <para>
        /// The method that compares the items in the collection to the searched element.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The zero-based index of item in the sorted collection, if item is found; otherwise -1.
        /// </para>
        /// </returns>       
        /// <exception cref="ArgumentNullException">
        /// <paramref name="predicate"/> is <see langword="null"/>.
        /// </exception>
        public int BinarySearch(SortedSearchPredicate<T> predicate)
        {
            return BinarySearch(0, base.Count, predicate);
        }

        /// <summary>
        /// Searches for an element that matches the conditions defined by the specified predicate, 
        /// and returns the zero-based index of the first occurrence within the entire collection.
        /// </summary>
        /// <param name="predicate">
        /// <para>
        /// Type: <see cref="Func{TSource, TResult}"/> of <typeparamref name="T"/>, <see cref="Boolean"/>
        /// </para>
        /// <para>
        /// The predicate that defines the conditions of the element to search for.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The zero-based index of the first occurrence of an element that matches the conditions defined by <paramref name="predicate"/>, if found; otherwise, –1.
        /// </para>
        /// </returns>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="predicate"/> is <see langword="null"/>.
        /// </exception>
        public int FindIndex(Func<T, bool> predicate)
        {
            return FindIndex(0, base.Count, predicate);
        }

        /// <summary>
        /// Searches for an element that matches the conditions defined by the specified predicate, 
        /// and returns the zero-based index of the first occurrence within the range of elements in the collection 
        /// that extends from the specified index to the last element.
        /// </summary>
        /// <param name="startIndex">
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The zero-based starting index of the search.
        /// </para>
        /// </param>
        /// <param name="predicate">
        /// <para>
        /// Type: <see cref="Func{TSource, TResult}"/> of <typeparamref name="T"/>, <see cref="Boolean"/>
        /// </para>
        /// <para>
        /// The predicate that defines the conditions of the element to search for.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The zero-based index of the first occurrence of an element that matches the conditions defined by <paramref name="predicate"/>, if found; otherwise, –1.
        /// </para>
        /// </returns>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="predicate"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ArgumentOutOfRangeException">
        /// <paramref name="startIndex"/> is outside the range of valid indexes for the collection.
        /// </exception>
        public int FindIndex(int startIndex, Func<T, bool> predicate)
        {
            return FindIndex(startIndex, base.Count - startIndex, predicate);
        }

        /// <summary>
        /// Searches for an element that matches the conditions defined by the specified predicate, 
        /// and returns the zero-based index of the first occurrence within the range of elements in the collection 
        /// that starts at the specified index and contains the specified number of elements
        /// </summary>
        /// <param name="startIndex">
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The zero-based starting index of the search.
        /// </para>
        /// </param>
        /// <param name="count">
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The number of elements in the section to search.
        /// </para>
        /// </param>
        /// <param name="predicate">
        /// <para>
        /// Type: <see cref="Func{TSource, TResult}"/> of <typeparamref name="T"/>, <see cref="Boolean"/>
        /// </para>
        /// <para>
        /// The predicate that defines the conditions of the element to search for.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The zero-based index of the first occurrence of an element that matches the conditions defined by <paramref name="predicate"/>, if found; otherwise, –1.
        /// </para>
        /// </returns>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="predicate"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ArgumentOutOfRangeException">
        /// <paramref name="startIndex"/> is outside the range of valid indexes for the collection.<br/>
        /// -or-<br/>
        /// <paramref name="count"/> is less than 0.<br/>
        /// -or-<br/>
        /// <paramref name="startIndex"/> and <paramref name="count"/> do not specify a valid section in the collection.
        /// </exception>
        public int FindIndex(int startIndex, int count, Func<T, bool> predicate)
        {
            if (count == 0)
                return -1;

            if (predicate == null)
                throw new ArgumentNullException("predicate");
            if (startIndex < 0 || startIndex >= base.Count)
                throw new ArgumentOutOfRangeException("startIndex");
            if (count < 0)
                throw new ArgumentOutOfRangeException("count");
            if (startIndex + count > base.Count)
                throw new ArgumentOutOfRangeException();

            for (int i = startIndex; i < count; ++i)
            {
                if (predicate(base.Items[i]))
                    return i;
            }

            return -1;
        }

        /// <summary>
        /// Moves the item at the specified index to a new location in the collection.
        /// </summary>
        /// <param name="oldIndex">
        /// <para>
        /// Type: <see cref="System.Int32"/>
        /// </para>
        /// <para>
        /// The zero-based index specifying the location of the item to be moved.
        /// </para>
        /// </param>
        /// <param name="newIndex">
        /// <para>
        /// Type: <see cref="System.Int32"/>
        /// </para>
        /// <para>
        /// The zero-based index specifying the new location of the item.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentOutOfRangeException">
        /// Either of the indices are not valid.
        /// </exception>
        public void Move(int oldIndex, int newIndex)
        {
            if (oldIndex == newIndex)
                return;
            if (oldIndex < 0 || oldIndex >= base.Count)
                throw new ArgumentOutOfRangeException("oldIndex", oldIndex, "The old index parameter is not valid!");
            if (newIndex < 0 || newIndex >= base.Count)
                throw new ArgumentOutOfRangeException("newIndex", newIndex, "The new index parameter is not valid!");
            
            MoveItem(oldIndex, newIndex);
        }

        #endregion
    }

    /// <summary>
    /// Represents a method that indicates whether a given item is equal to or less or greater than a searched object.
    /// </summary>
    /// <remarks>
    /// This method is used as an indicator for the <see cref="ObservableCollection{T}.BinarySearch(SortedSearchPredicate{T})"/> method
    /// that performs a binary search in a sorted collection.
    /// </remarks>
    /// <typeparam name="T">The type of the compared items</typeparam>
    /// <param name="other">
    /// <para>
    /// Type: <typeparamref name="T"/>
    /// </para>
    /// <para>
    /// The item that is being compared
    /// </para>
    /// </param>
    /// <returns>
    /// <para>
    /// Type: <see cref="Int32"/>
    /// </para>
    /// <para>
    /// <list type="table">
    /// <listheader>
    /// <term>
    ///     Value
    /// </term>
    /// <description>
    ///     Condition
    /// </description>
    /// </listheader>
    /// <item>
    /// <term>
    ///     Less than 0
    /// </term>
    /// <description>
    ///     <paramref name="other"/> is less than the searched item.
    /// </description>
    /// </item>
    /// <item>
    /// <term>
    ///     Equal to 0
    /// </term>
    /// <description>
    ///     <paramref name="other"/> is equal to the searched item.
    /// </description>
    /// </item>
    /// <item>
    /// <term>
    ///     Greater than 0
    /// </term>
    /// <description>
    ///     <paramref name="other"/> is greater than the searched item.
    /// </description>
    /// </item>
    /// </list>
    /// </para>
    /// </returns>
    public delegate int SortedSearchPredicate<T>(T other);
}
